﻿// -----------------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
// -----------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading;
using Microsoft.Internal;
using System.ComponentModel.Composition.Caching;
using System.ComponentModel.Composition.Caching.AttributedModel;

namespace System.ComponentModel.Composition
{
    public class DirectoryPartCatalog : ComposablePartCatalog, INotifyComposablePartCatalogChanged, ICachedComposablePartCatalog
    {
        private readonly Lock _thisLock = new Lock();
        private volatile AggregatingComposablePartCatalog _innerCatalog;
        private Dictionary<string, AttributedAssemblyPartCatalog> _assemblyCatalogs;
        private SynchronizationContext _syncContext;
        private List<FileSystemWatcher> _watchers;
        private volatile bool _disposed = false;
        private EventHandler<ComposablePartCatalogChangedEventArgs> _changedEventStorage;
        private EventHandler<EventArgs> _changingEventStorage;
        private string _path;
        private string _filter;
        private bool _watchDirectory;
        private ComposablePartCatalogCache _cache;
        private IDictionary<string, object> _cacheSubordinateTokens;
        private bool _isFullyLoadedFromCache = false;

        public DirectoryPartCatalog(string path)
        {
            this.Initialize(path, null, false);
        }

        public DirectoryPartCatalog(string path, bool watchDirectory)
        {
            this.Initialize(path, null, watchDirectory);
        }

        public DirectoryPartCatalog(string path, string filter, bool watchDirectory)
        {
            this.Initialize(path, filter, watchDirectory);
        }

        public DirectoryPartCatalog(ComposablePartCatalogCache cache)
        {
            Requires.NotNull(cache, "cache");

            this._isFullyLoadedFromCache = true;
            this._cache = cache;
            this._cacheSubordinateTokens = cache.Metadata.ReadDictionary<object>(AttributedCacheServices.CacheKeys.SubordinateTokens);
            this.Initialize(
                cache.Metadata.ReadValue<string>(AttributedCacheServices.CacheKeys.Path),
                cache.Metadata.ReadValue<string>(AttributedCacheServices.CacheKeys.Filter),
                cache.Metadata.ReadValue<bool>(AttributedCacheServices.CacheKeys.WatchDirectory));

            // if all assemblies we have ever loaded has been loaded from cache AND those are the only assemblies we have ever loaded
            // then we are fully loaded from cache
            this._isFullyLoadedFromCache &= (this._cacheSubordinateTokens.Count == this._assemblyCatalogs.Count);
        }

        public override IQueryable<ComposablePartDefinition> Parts
        {
            get
            {
                this.ThrowIfDisposed();
                return this._innerCatalog.Parts;
            }
        }

        public override IEnumerable<Tuple<ComposablePartDefinition, ExportDefinition>> GetExports(Expression<Func<ExportDefinition, bool>> constraint)
        {
            ThrowIfDisposed();

            Requires.NotNull(constraint, "constraint");

            return this._innerCatalog.GetExports(constraint);
        }

        private void Initialize(string path, string filter, bool watchDirectory)
        {
            this._path = path;
            this._filter = filter;
            this._watchDirectory = watchDirectory;

            string[] filters;

            this._innerCatalog = new AggregatingComposablePartCatalog();
            this._assemblyCatalogs = new Dictionary<string, AttributedAssemblyPartCatalog>();
            this._syncContext = SynchronizationContext.Current;
            path = DirectoryPartCatalog.GetNormalizedDirectoryName(path);
            if (string.IsNullOrEmpty(filter))
            {
                string[] filterArray = { "*.EXE", "*.DLL" };
                filters = filterArray;
            }
            else
            {
                string[] filterArray = { filter };
                filters = filterArray;
            }

            this._watchers = new List<FileSystemWatcher>();

            this._innerCatalog.Changed += OnChanged;
            this._innerCatalog.Changing += OnChanging;

            foreach (string f in filters)
            {
                if (watchDirectory)
                {
                    var fsw = new FileSystemWatcher(path);

                    fsw.Created += new FileSystemEventHandler(this.OnFileCreated);
                    fsw.Deleted += new FileSystemEventHandler(this.OnFileDeleted);
                    fsw.EnableRaisingEvents = true;
                    fsw.IncludeSubdirectories = false;
                    fsw.Filter = f;
                    this._watchers.Add(fsw);
                }

                foreach (string file in Directory.GetFiles(path, f))
                {
                    this.AddAssemblyGuarded(file);
                }
            }

        }

        /// <summary>
        ///    Caches the catalog using the specified cache writer
        /// </summary>
        /// <param name="writer">
        ///    The <see cref="ComposablePartCatalogCacheWriter"/> to use to cache the catalog.
        /// </param>
        /// <returns></returns>
        public object CacheCatalog(ComposablePartCatalogCacheWriter writer)
        {
            ThrowIfDisposed();

            Requires.NotNull(writer, "writer");

            // we will cache the following information
            // 1. Directory name AS PASSED - that is, if a relative path is passed, that's what we will cache
            // 2. The filter, as passed
            // 3. The watchDirectory, as passed
            // 4. Mapping of assembly names to cache tokens
            Dictionary<string, object> catalogMetadata = new Dictionary<string, object>();
            catalogMetadata.WriteValue(AttributedCacheServices.CacheKeys.Path, this._path);
            catalogMetadata.WriteValue(AttributedCacheServices.CacheKeys.Filter, this._filter);
            catalogMetadata.WriteValue(AttributedCacheServices.CacheKeys.WatchDirectory, this._watchDirectory);

            IDictionary<string, object> subordinateTokens = new Dictionary<string, object>();
            foreach (KeyValuePair<string, AttributedAssemblyPartCatalog> kvp in this._assemblyCatalogs)
            {
                // the assembly paths have been already normalized to upper, so no need to do that again
                string assemblyFileName = Path.GetFileName(kvp.Key);
                object subordinateCacheToken = kvp.Value.CacheCatalog(writer);
                subordinateTokens.Add(assemblyFileName, subordinateCacheToken);
            }

            catalogMetadata.WriteDictionary(AttributedCacheServices.CacheKeys.SubordinateTokens, subordinateTokens);

            object cacheToken = writer.WriteCache(
                                    this.GetType(),
                                    null,
                                    catalogMetadata,
                                    null);

            writer.WriteRootCacheToken(cacheToken);

            return cacheToken;
        }

        public bool IsCacheUpToDate
        {
            get
            {
                ThrowIfDisposed();

                // if we are not loaded from cache, then we are for sure not up to date
                if (!this._isFullyLoadedFromCache)
                {
                    return false;
                }

                // all catalogs should report that they have been loaded from cache (in case the actul assemblies have changed)
                foreach (AttributedAssemblyPartCatalog subordinateCatalog in this._assemblyCatalogs.Values)
                {
                    if (!subordinateCatalog.IsCacheUpToDate)
                    {
                        return false;
                    }
                }

                return true;
            }
        }

        private void AddAssemblyGuarded(string assemblyFilePath)
        {
            assemblyFilePath = assemblyFilePath.ToUpperInvariant();

            try
            {
                AttributedAssemblyPartCatalog assemblyCatalog = null;
                bool assemblyLoadedFromCache = true;
                if (!this.TryGetAssemblyCatalogFromCache(assemblyFilePath, out assemblyCatalog))
                {
                    assemblyCatalog = new AttributedAssemblyPartCatalog(assemblyFilePath);
                    assemblyLoadedFromCache = false;
                }

                using (new WriteLock(this._thisLock))
                {
                    this._assemblyCatalogs.Add(assemblyFilePath, assemblyCatalog);
                    if (!assemblyLoadedFromCache)
                    {
                        this._isFullyLoadedFromCache = false;
                    }
                }
                this._innerCatalog.Catalogs.Add(assemblyCatalog);
            }
            catch (FileNotFoundException)
            {
                // Files should always exists but don't blow up here if they don't
            }
            catch (BadImageFormatException)
            {
                // Don't blow up if we run across a native assembly
            }
        }

        private bool TryGetAssemblyCatalogFromCache(string assemblyFilePath, out AttributedAssemblyPartCatalog assemblyCatalog)
        {
            Assumes.NotNull(assemblyFilePath);

            assemblyCatalog = null;

            if (this._cache == null)
            {
                return false;
            }
            Assumes.NotNull(this._cacheSubordinateTokens);

            // check if the cache knows anything about the assembly
            string assemblyFileName = Path.GetFileName(assemblyFilePath);
            object assemblyCacheToken = null;
            if (!this._cacheSubordinateTokens.TryGetValue(assemblyFileName, out assemblyCacheToken))
            {
                return false;
            }

            assemblyCatalog = (AttributedAssemblyPartCatalog)this._cache.Reader.ReadCatalog(assemblyCacheToken);
            return true;
        }

        private void RemoveAssemblyGuarded(string assemblyFilePath)
        {
            assemblyFilePath = assemblyFilePath.ToUpperInvariant();

            AttributedAssemblyPartCatalog assemblyCatalog;
            bool found;
            using (new WriteLock(this._thisLock))
            {
                found = this._assemblyCatalogs.TryGetValue(assemblyFilePath, out assemblyCatalog);
                if (found)
                {
                    this._assemblyCatalogs.Remove(assemblyFilePath);
                }
                this._isFullyLoadedFromCache = false;
            }
            if (found)
            {
                this._innerCatalog.Catalogs.Remove(assemblyCatalog);
            }
        }

        private static string GetRelativeDirectory(string relativePath)
        {
            string directory = relativePath;

            string baseDir = AppDomain.CurrentDomain.BaseDirectory;

            if (!string.IsNullOrEmpty(baseDir))
            {
                directory = Path.Combine(Path.GetDirectoryName(baseDir), relativePath);
            }

            return directory;
        }


        private void OnChanged(object sender, ComposablePartCatalogChangedEventArgs e)
        {
            EventHandler<ComposablePartCatalogChangedEventArgs> changedEvent;
            ThrowIfDisposed();

            using (new ReadLock(this._thisLock))
            {
                changedEvent = this._changedEventStorage;
            }

            if (changedEvent != null)
            {
                SynchronizationContextHelpers.ExecuteOnCreationThread(this._syncContext, delegate
                {
                    changedEvent(sender, e);
                });
            }
        }

        private void OnChanging(object sender, EventArgs e)
        {
            EventHandler<EventArgs> changingEvent;

            using (new ReadLock(this._thisLock))
            {

                changingEvent = this._changingEventStorage;
            }

            if (changingEvent != null)
            {
                SynchronizationContextHelpers.ExecuteOnCreationThread(this._syncContext, delegate
                {
                    changingEvent(sender, e);
                });
            }
        }

        private void OnFileCreated(object sender, FileSystemEventArgs e)
        {
            this.AddAssemblyGuarded(e.FullPath);
        }

        private void OnFileDeleted(object sender, FileSystemEventArgs e)
        {
            this.RemoveAssemblyGuarded(e.FullPath);
        }

        // Paths are either Absolute, or relative to the AppDomainBase directory
        private static string GetNormalizedDirectoryName(string path)
        {
            if (string.IsNullOrEmpty(path))
            {
                path = GetRelativeDirectory("");
            }
            else
            {
                // Were we given an existing directory
                if (!Directory.Exists(path))
                {
                    // Were we given a file name
                    string dir = Path.GetDirectoryName(path);
                    if (Directory.Exists(dir))
                    {
                        path = dir;
                    }
                    else
                    {
                        // Compute relative directory from AppDomain.ApplicationBase
                        path = GetRelativeDirectory(path);
    
                        if (!Directory.Exists(path))
                        {
                            throw new DirectoryNotFoundException(string.Format(CultureInfo.CurrentCulture, Strings.DirectoryNotFound, path));
                        }
                    }
                }
            }

            return Path.GetFullPath(path).ToUpperInvariant();
        }


        #region INotifyComposablePartCatalogChanged Members
        /// <summary>
        /// Notify when the contents of the Catalog has changed.
        /// </summary>
        public event EventHandler<ComposablePartCatalogChangedEventArgs> Changed
        {
            add
            {
                ThrowIfDisposed();

                using (new WriteLock(this._thisLock))
                {
                    this._changedEventStorage += value;
                }
            }
            remove
            {
                ThrowIfDisposed();

                using (new WriteLock(this._thisLock))
                {
                    this._changedEventStorage -= value;
                }
            }
        }

        /// <summary>
        /// Notify when the contents of the Catalog is about to change.
        /// </summary>
        public event EventHandler<EventArgs> Changing
        {
            add
            {
                this.ThrowIfDisposed();

                using (new WriteLock(this._thisLock))
                {
                    this._changingEventStorage += value;
                }
            }
            remove
            {
                ThrowIfDisposed();

                using (new WriteLock(this._thisLock))
                {
                    this._changingEventStorage -= value;
                }
            }
        }
        #endregion

        protected override void Dispose(bool disposing)
        {
            try
            {
                if (disposing)
                {
                    if (!this._disposed)
                    {
                        bool disposeLock = false;
                        try
                        {
                            using (new WriteLock(this._thisLock))
                            {
                                if (!this._disposed)
                                {
                                    disposeLock = true;
                                    foreach (var fsw in this._watchers)
                                    {
                                        fsw.Dispose();
                                    }
                                    this._watchers.Clear();
                                    this._innerCatalog.Dispose();
                                    this._disposed = true;
                                }
                            }
                        }
                        finally
                        {
                            if (disposeLock)
                            {
                                this._thisLock.Dispose();
                            }
                        }
                    }
                }
            }
            finally
            {
                base.Dispose(disposing);
            }
        }

        private void ThrowIfDisposed()
        {
            if (_disposed)
            {
                throw ExceptionBuilder.CreateObjectDisposed(this);
            }
        }

    }
}
